{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Interactive art with ipywidgets and Flat\n",
    "\n",
    "By [Allison Parrish](http://www.decontextualize.com/)\n",
    "\n",
    "This notebook shows how to add interactivity to Python code that produces visual output with [Flat](https://xxyxyz.org/flat). It's meant to accompany course material in [Material of Language](https://github.com/aparrish/material-of-language/), but my hope is it will be valuable to anyone who wants to learn the basics of [ipywidgets](https://github.com/jupyter-widgets/ipywidgets).\n",
    "\n",
    "The ipywidgets provides a set of functions for adding interactive HTML controls to your Jupyter Notebook. It also provides an interface between those controls and the Python kernel. This makes it possible to change the value of global variables in the notebook by (e.g.) moving a slider, or to call a Python function whose parameters are determined by a number of controls.\n",
    "\n",
    "Before you begin, make sure you understand [Python functions](https://docs.python.org/3/tutorial/controlflow.html#defining-functions) ([my function tutorial](http://rwet.decontextualize.com/book/functions/)).\n",
    "\n",
    "> Note: If you're viewing this on GitHub, you'll see the widgets render, but they won't do anything! That's because GitHub is displaying a static exported copy of the notebook in your browser—there's no Python kernel running behind the scenes, so the code won't do anything. If you want to see the widgets in action, view this notebook on your own computer or [launch this notebook on Binder](https://mybinder.org/v2/gh/aparrish/material-of-language/master).\n",
    "\n",
    "## Preliminaries\n",
    "\n",
    "First, make sure you have `ipywidgets` installed, [following the instructions here](https://github.com/jupyter-widgets/ipywidgets#install). If you're using Anaconda:\n",
    "\n",
    "    conda install -c conda-forge ipywidgets\n",
    "    \n",
    "You'll also want to install [Flat](https://xxyxyz.org/flat) via pip:\n",
    "\n",
    "    pip install flat\n",
    "    \n",
    "And [Bezmerizing](https://github.com/aparrish/bezmerizing/) (not yet on PyPI):\n",
    "\n",
    "    pip install https://github.com/aparrish/bezmerizing/archive/master.zip\n",
    "    \n",
    "The following cells perform all of the necessary imports:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import ipywidgets as widgets\n",
    "from IPython.display import display"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "from flat import document, shape, rgb, rgba\n",
    "from bezmerizing import Polyline"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The function below is a convenience function for taking a Flat `page` object and displaying its rendered SVG to the notebook:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "from IPython.display import SVG, display\n",
    "def show(page):\n",
    "    display(SVG(page.svg()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Widgets\n",
    "\n",
    "The ipywidgets library provides a number of HTML controls (\"widgets\"). [There's a full list here](https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html). I'm just going to pick and choose a few handy and interesting controls below. To create an HTML control and display it in the notebook, call the widget's constructor function and pass the resulting value to the IPython `display()` function. The code in the cell below creates a simple slider and displays it:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "31480602ab164314a16648ce035212f9",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "IntSlider(value=5, description='# of wugs', max=10)"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "int_slider = widgets.IntSlider(\n",
    "    description=\"# of wugs\",\n",
    "    value=5,\n",
    "    min=0,\n",
    "    max=10)\n",
    "display(int_slider)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Most widgets take a named parameter called `description`, which usually determines the text of the label next to the widget when it's displayed. Every widget has a slightly different set of parameters; [consult the documentation](https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html) for more information about what parameters are available and what they mean. For the `IntSlider`, the `min` parameter sets the slider's minimum value; the `max` parameter sets the maximum value; and the `value` parameter sets the initial value.\n",
    "\n",
    "Widget objects also usually have `.value` attribute that evaluates to the current value of the widget—in this case, the position of the slider:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "7"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "int_slider.value"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note that if you manipulate the slider and re-run the cell above, the value will change accordingly!\n",
    "\n",
    "The `FloatSlider` widget is like `IntSlider`, except it produces a floating-point number instead of an integer. It has many of the same parameters as `IntSlider`, including the `step` parameter that I use below to control the largest change in value the slider can make when you move it:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "26ee7d3dc57b47149fe0d7417562f93a",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "FloatSlider(value=0.5, description='dairy content', max=1.0, step=0.05)"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "float_slider = widgets.FloatSlider(\n",
    "    description=\"dairy content\",\n",
    "    value=0.5,\n",
    "    min=0,\n",
    "    max=1,\n",
    "    step=0.05)\n",
    "display(float_slider)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Like the `IntSlider`, you can check the slider position with its `.value` attribute:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "0.5"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "float_slider.value"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here are a few more widgets, to help you understand the possibilities. A text input widget:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "f29698d42f0145d78f054ca7b560baeb",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Text(value='', description='your name', placeholder='type something...')"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "text_input = widgets.Text(\n",
    "    description=\"your name\",\n",
    "    placeholder=\"type something...\")\n",
    "display(text_input)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "''"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "text_input.value"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "A checkbox, whose `.value` is either `True` (checked) or `False` (unchecked):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "23fbf1c6045d4cf286bc53270c5cad87",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Checkbox(value=False, description='Ketchup?')"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "checkbox = widgets.Checkbox(\n",
    "    description=\"Ketchup?\",\n",
    "    value=False\n",
    ")\n",
    "display(checkbox)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "checkbox.value"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And a color picker, which will open your operating system's color picker dialog, and evaluates to a HTML hex color:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "726fc303eb08438daee285bf6335726b",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "ColorPicker(value='black')"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "color_pick = widgets.ColorPicker()\n",
    "display(color_pick)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'#7f50c7'"
      ]
     },
     "execution_count": 28,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "color_pick.value"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We'll be using this value in Flat, which doesn't have a built-in means to convert hex triplets to RGB values. The following function will do the job:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[0, 249, 0]"
      ]
     },
     "execution_count": 20,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def hex2rgb(s):\n",
    "    s = s.lstrip(\"#\")\n",
    "    return [int(s[i:i+2], 16) for i in range(0,5,2)]\n",
    "hex2rgb(color_pick.value)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Making use of widget values\n",
    "\n",
    "You can use the `.value` attribute of the widget anywhere you'd like. The following cell has a simple Flat drawing that uses the values of the sliders and color picker above to draw an ellipse. If you change the values above and re-run the cell, you'll see how your changes affect the drawing."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/svg+xml": [
       "<svg height=\"226.7717pt\" version=\"1.1\" width=\"226.7717pt\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
       "<title>Untitled</title>\n",
       "<ellipse cx=\"113.3858\" cy=\"113.3858\" fill=\"rgb(0,249,0)\" rx=\"56.6929\" ry=\"56.6929\" stroke=\"rgb(0,0,0)\" stroke-miterlimit=\"10\" stroke-width=\"7\"/>\n",
       "</svg>"
      ],
      "text/plain": [
       "<IPython.core.display.SVG object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "page = document(80, 80, 'mm').addpage()\n",
    "pen = (shape()\n",
    "       .fill(rgb(*hex2rgb(color_pick.value)))\n",
    "       .stroke(rgb(0,0,0))\n",
    "       .width(int_slider.value))\n",
    "page.place(pen.ellipse(40, 40,\n",
    "                       40*float_slider.value,\n",
    "                       40*float_slider.value))\n",
    "show(page)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Interactive functions\n",
    "\n",
    "This is fine as far as it goes, but it's only marginally \"interactive.\" Ideally, there would be some way to re-run a chunk of code whenever the user changes the widget, instead of having to re-run it manually.\n",
    "\n",
    "Now we've got everything we need in place to show how to make an interactive interface. The easiest way to do this with IPython widgets involves turning the code that we want to make interactive into a *function* first. If you're not familiar with how Python functions work, I recommend [this tutorial I wrote a while back](http://rwet.decontextualize.com/book/functions/).\n",
    "\n",
    "Let's start with a simple example. The following cell defines a function that uses Flat to draw and display a rectangle. The function has one parameter, `size`, which sets the size of the square:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [],
   "source": [
    "def disp_square(size=40):\n",
    "    page = document(80, 80, 'mm').addpage()\n",
    "    pen = shape().fill(rgb(120, 40, 160)).nostroke()\n",
    "    page.place(pen.rectangle(10, 10, size, size))\n",
    "    show(page)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Because the function has a default value, it works without any parameters:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/svg+xml": [
       "<svg height=\"226.7717pt\" version=\"1.1\" width=\"226.7717pt\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
       "<title>Untitled</title>\n",
       "<rect fill=\"rgb(120,40,160)\" height=\"113.3858\" width=\"113.3858\" x=\"28.3465\" y=\"28.3465\"/>\n",
       "</svg>"
      ],
      "text/plain": [
       "<IPython.core.display.SVG object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "disp_square()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Or you can specify the parameter:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/svg+xml": [
       "<svg height=\"226.7717pt\" version=\"1.1\" width=\"226.7717pt\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
       "<title>Untitled</title>\n",
       "<rect fill=\"rgb(120,40,160)\" height=\"56.6929\" width=\"56.6929\" x=\"28.3465\" y=\"28.3465\"/>\n",
       "</svg>"
      ],
      "text/plain": [
       "<IPython.core.display.SVG object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "disp_square(size=20)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To make this function interactive, you need to do two things. First: create a widget for the parameter (`IntSlider` in this case). Then call the `widgets.interact()` function. The first parameter of `widgets.interact()` should be the function that you want to call interactively. The remaining (named) parameters map names of your function's parameters to the widget whose value will be passed to that parameter. The cell below does the work—run it, then move the slider to see how it works:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "7e1921f74fbf4571ab764b20e39f76f3",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "interactive(children=(IntSlider(value=35, description='size', max=70), Output()), _dom_classes=('widget-intera…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "<function __main__.disp_square>"
      ]
     },
     "execution_count": 27,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "size_slider = widgets.IntSlider(min=0, max=70, value=35)\n",
    "widgets.interact(disp_square, size=size_slider)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here's a more sophisticated example. The `disp_ellipse()` function below draws an ellipse, using values from several parameters to determine color, stroke width, and size:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [],
   "source": [
    "def disp_ellipse(fill_c=\"#804080\", stroke_width=10, ellipse_size=20):\n",
    "    page = document(80, 80, 'mm').addpage()\n",
    "    pen = (shape()\n",
    "           .fill(rgb(*hex2rgb(fill_c)))\n",
    "           .stroke(rgb(0,0,0))\n",
    "           .width(stroke_width))\n",
    "    page.place(pen.ellipse(40, 40,\n",
    "                           ellipse_size,\n",
    "                           ellipse_size))\n",
    "    show(page)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/svg+xml": [
       "<svg height=\"226.7717pt\" version=\"1.1\" width=\"226.7717pt\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
       "<title>Untitled</title>\n",
       "<ellipse cx=\"113.3858\" cy=\"113.3858\" fill=\"rgb(0,0,255)\" rx=\"113.3858\" ry=\"113.3858\" stroke=\"rgb(0,0,0)\" stroke-miterlimit=\"10\" stroke-width=\"10\"/>\n",
       "</svg>"
      ],
      "text/plain": [
       "<IPython.core.display.SVG object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "disp_ellipse(\"#0000ff\", 10, 40)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In the example below, I make this an interactive function using `widgets.interact()`. Here, I don't bother assigning the widget objects to their own variables, passing them instead directly as values for the named parameters:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "8929b9553de5456a826e8514a806c34c",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "interactive(children=(ColorPicker(value='#804080', description='fill_c'), IntSlider(value=10, description='str…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "<function __main__.disp_ellipse>"
      ]
     },
     "execution_count": 30,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "widgets.interact(\n",
    "    disp_ellipse,\n",
    "    fill_c=widgets.ColorPicker(value=\"#804080\"),\n",
    "    stroke_width=widgets.IntSlider(min=0, max=20, value=10),\n",
    "    ellipse_size=widgets.IntSlider(min=0, max=100, value=20))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Slowing it down\n",
    "\n",
    "You'll notice a little bit of a lag between the movement of the slider and the visual being displayed. This is because Flat isn't really meant to draw in real time, and sometimes it takes longer for Flat to render the visual than it takes ipywidgets to call the function again—so the calls get backed up. This is usually fine, but it might be distracting. One way to fix it is to pass `continuous_update=False` to the slider object constructor. This tells ipywidgets to only call the function when the user has released the mouse, instead of whenever the value changes. Here's the same example as above, but using `continuous_update=False`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "e0bdcb308e8c407193966a17ac0d91e4",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "interactive(children=(ColorPicker(value='#804080', description='fill_c'), IntSlider(value=10, continuous_updat…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "<function __main__.disp_ellipse>"
      ]
     },
     "execution_count": 31,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "widgets.interact(\n",
    "    disp_ellipse,\n",
    "    fill_c=widgets.ColorPicker(value=\"#804080\"),\n",
    "    stroke_width=widgets.IntSlider(min=0, max=20, value=10, continuous_update=False),\n",
    "    ellipse_size=widgets.IntSlider(min=0, max=100, value=20, continuous_update=False))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Seeding random\n",
    "\n",
    "One potential issue with interactive procedural art is randomness. The ipywidgets `widgets.interact()` function calls your function whenever the slider changes. But if you use random values in that function, each call will produce something drastically different, owing to the different random values. This might be what you want, but it's a little bit unintuitive. As a quick example, consider the function below, which draws *n* ellipses with random fill colors and positions:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "outputs": [],
   "source": [
    "def rand_ellipses(n=10):\n",
    "    page = document(80, 80, 'mm').addpage()\n",
    "    for i in range(n):\n",
    "        r = np.random.uniform(255)\n",
    "        g = np.random.uniform(255)\n",
    "        b = np.random.uniform(255)\n",
    "        x = np.random.uniform(80)\n",
    "        y = np.random.uniform(80)\n",
    "        size = np.random.uniform(40)\n",
    "        pen = shape().nostroke().fill(rgb(r, g, b))\n",
    "        page.place(pen.ellipse(x, y, size, size))\n",
    "    show(page)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Each time you run it, you'll get something different:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/svg+xml": [
       "<svg height=\"226.7717pt\" version=\"1.1\" width=\"226.7717pt\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
       "<title>Untitled</title>\n",
       "<ellipse cx=\"29.6914\" cy=\"131.5366\" fill=\"rgb(178.6196,83.4892,207.6501)\" rx=\"72.5243\" ry=\"72.5243\"/>\n",
       "<ellipse cx=\"186.7807\" cy=\"64.5275\" fill=\"rgb(116.3173,215.9943,227.3404)\" rx=\"98.6343\" ry=\"98.6343\"/>\n",
       "<ellipse cx=\"103.8042\" cy=\"24.7052\" fill=\"rgb(171.5633,123.59,218.1439)\" rx=\"33.4227\" ry=\"33.4227\"/>\n",
       "<ellipse cx=\"110.0848\" cy=\"215.5429\" fill=\"rgb(116.7499,47.5369,133.6379)\" rx=\"13.5198\" ry=\"13.5198\"/>\n",
       "<ellipse cx=\"58.9226\" cy=\"158.4761\" fill=\"rgb(227.59,248.2196,157.7502)\" rx=\"112.5645\" ry=\"112.5645\"/>\n",
       "<ellipse cx=\"114.3691\" cy=\"156.3705\" fill=\"rgb(30.5323,206.9642,95.3668)\" rx=\"34.3654\" ry=\"34.3654\"/>\n",
       "<ellipse cx=\"83.7717\" cy=\"170.6223\" fill=\"rgb(66.6385,111.2219,98.1397)\" rx=\"100.1696\" ry=\"100.1696\"/>\n",
       "<ellipse cx=\"168.4873\" cy=\"116.2061\" fill=\"rgb(54.0893,49.7352,171.6794)\" rx=\"36.0582\" ry=\"36.0582\"/>\n",
       "<ellipse cx=\"131.3218\" cy=\"132.8174\" fill=\"rgb(231.0364,148.4049,85.2289)\" rx=\"91.6996\" ry=\"91.6996\"/>\n",
       "<ellipse cx=\"180.9619\" cy=\"53.6093\" fill=\"rgb(186.4926,208.302,242.0696)\" rx=\"100.919\" ry=\"100.919\"/>\n",
       "</svg>"
      ],
      "text/plain": [
       "<IPython.core.display.SVG object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "rand_ellipses(10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Hook this up to `widgets.interact()`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "1284e761e95e46d4a2592e086b26b569",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "interactive(children=(IntSlider(value=10, description='n'), Output()), _dom_classes=('widget-interact',))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "<function __main__.rand_ellipses>"
      ]
     },
     "execution_count": 36,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "widgets.interact(rand_ellipses, n=widgets.IntSlider(min=0, max=100, value=10))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Each time you move the slider, you get a different set of ellipses! This makes sense, but you might instead want for movements of the slider to simply add/remove ellipses, not create a completely new set. An easy way to do this is to *seed* the random number generator. Seeding the random number generator ensures that the sequence of random numbers produced from the generator will be the same between subsequent runs of the same code. The code in the cell below starts off by calling `np.random.seed()`, which seeds numpy's random number generator. Each time you run the cell, you'll get the same numbers. Try changing the seed:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0.43211120560090255\n",
      "0.17421526319883063\n",
      "0.17094368527750203\n",
      "0.8276322476206412\n",
      "0.587171260125725\n",
      "0.459353745644712\n",
      "0.82268475801786\n",
      "0.8215481108679935\n",
      "0.3071202514971392\n",
      "0.20089437575483526\n"
     ]
    }
   ],
   "source": [
    "np.random.seed(103)\n",
    "for i in range(10):\n",
    "    print(np.random.uniform())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The function in the cell is like the function above, except it sets the random seed at the top. This ensures that function will always produce the same sequence of random numbers—and therefore ensures that it will always show the same visual output. (But you can change the seed later if you want different output. One of the examples below lets you pick the seed with a widget!)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "metadata": {},
   "outputs": [],
   "source": [
    "def rand_ellipses_seed(n=10):\n",
    "    np.random.seed(12345)\n",
    "    page = document(80, 80, 'mm').addpage()\n",
    "    for i in range(n):\n",
    "        r = np.random.uniform(255)\n",
    "        g = np.random.uniform(255)\n",
    "        b = np.random.uniform(255)\n",
    "        x = np.random.uniform(80)\n",
    "        y = np.random.uniform(80)\n",
    "        size = np.random.uniform(40)\n",
    "        pen = shape().nostroke().fill(rgb(r, g, b))\n",
    "        page.place(pen.ellipse(x, y, size, size))\n",
    "    show(page)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/svg+xml": [
       "<svg height=\"226.7717pt\" version=\"1.1\" width=\"226.7717pt\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
       "<title>Untitled</title>\n",
       "<ellipse cx=\"180.963\" cy=\"99.637\" fill=\"rgb(18.8775,174.6406,208.2846)\" rx=\"47.5477\" ry=\"47.5477\"/>\n",
       "<ellipse cx=\"80.4132\" cy=\"59.3306\" fill=\"rgb(10.0133,89.093,64.7777)\" rx=\"7.1122\" ry=\"7.1122\"/>\n",
       "<ellipse cx=\"79.7769\" cy=\"45.4247\" fill=\"rgb(252.8694,227.9631,179.1293)\" rx=\"16.9657\" ry=\"16.9657\"/>\n",
       "<ellipse cx=\"66.1072\" cy=\"122.0589\" fill=\"rgb(9.9795,71.1839,91.8113)\" rx=\"77.3921\" ry=\"77.3921\"/>\n",
       "<ellipse cx=\"75.1946\" cy=\"49.6772\" fill=\"rgb(143.3303,69.659,2.5203)\" rx=\"94.4911\" ry=\"94.4911\"/>\n",
       "<ellipse cx=\"221.2457\" cy=\"116.6512\" fill=\"rgb(248.1803,51.706,25.4545)\" rx=\"55.2077\" ry=\"55.2077\"/>\n",
       "<ellipse cx=\"63.6859\" cy=\"43.5128\" fill=\"rgb(103.523,241.8028,27.6473)\" rx=\"58.0856\" ry=\"58.0856\"/>\n",
       "<ellipse cx=\"168.8349\" cy=\"121.9455\" fill=\"rgb(49.2119,230.624,199.3867)\" rx=\"62.6016\" ry=\"62.6016\"/>\n",
       "<ellipse cx=\"189.208\" cy=\"54.6058\" fill=\"rgb(74.7845,209.7745,120.0117)\" rx=\"10.7755\" ry=\"10.7755\"/>\n",
       "<ellipse cx=\"142.2702\" cy=\"36.7384\" fill=\"rgb(100.1886,216.8534,130.6348)\" rx=\"12.663\" ry=\"12.663\"/>\n",
       "</svg>"
      ],
      "text/plain": [
       "<IPython.core.display.SVG object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "rand_ellipses_seed()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Hooking this up to `widgets.interact()`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 47,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "e154d83ca82a4520bfa772bf7d44c23f",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "interactive(children=(IntSlider(value=10, description='n'), Output()), _dom_classes=('widget-interact',))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "<function __main__.rand_ellipses_seed>"
      ]
     },
     "execution_count": 47,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "widgets.interact(rand_ellipses_seed,\n",
    "                 n=widgets.IntSlider(min=0, max=100, value=10))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can see that moving the slider changes only the number of ellipses, not their color and position. Success!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "[Python's built-in `random` module has a similar function, `seed()`](https://docs.python.org/3/library/random.html#random.seed). Note that Python's built-in random generator and numpy's random number generator are distinct—seeding one doesn't seed the other! Also note that certain other "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Quick interactivity with `interact` decorator\n",
    "\n",
    "It's very common to want to make a function interactive without a lot of fuss, or to write a function with no other intention than to make it interactive. To make this common use case easier, the ipywidgets library includes a [decorator](https://realpython.com/primer-on-python-decorators/) that you can put in front of a function to make it instantly interactive. The code in the cell below demonstrates. The `disp_tri()` function displays a triangle with the specified stroke width and height. Putting the `@widgets.interact` decorator above it creates an interactive interface for the function:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "83b557b57ef54782b16384d98fc8809b",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "interactive(children=(IntSlider(value=10, description='stroke_width', max=30, min=-10), IntSlider(value=20, de…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "@widgets.interact\n",
    "def disp_tri(stroke_width=10, height=20):\n",
    "    page = document(80, 80, 'mm').addpage()\n",
    "    pen = shape().nofill().stroke(rgb(0,0,0)).width(stroke_width)\n",
    "    tri = Polyline([(40, 20), (20, 20+height), (60, 20+height)])\n",
    "    page.place(pen.polygon(tri))\n",
    "    show(page)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Whoa, super convenient! The drawback of this technique is that ipywidgets has to guess about the minimum, maximum and initial value of the sliders. It has to perform similar guesses for functions with parameters of other types. And sometimes it gets it wrong—as it does in the example above, where it lets us make a negative stroke width. To fix this, you can pass [widget abbreviations](https://ipywidgets.readthedocs.io/en/latest/examples/Using%20Interact.html#Widget-abbreviations) to the `widgets.interact` decorator, to clarify what kinds of widgets should go along with the function's corresponding parameters:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "addb47a2f151402794da242eff791e2f",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "interactive(children=(FloatSlider(value=10.0, description='stroke_width', max=20.0, min=0.5, step=0.5), IntSli…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "@widgets.interact(stroke_width=(0.5, 20, 0.5), height=(20, 60))\n",
    "def disp_tri(stroke_width=10, height=20):\n",
    "    page = document(80, 80, 'mm').addpage()\n",
    "    pen = shape().nofill().stroke(rgb(0,0,0)).width(stroke_width)\n",
    "    tri = Polyline([(40, 20), (20, 20+height), (60, 20+height)])\n",
    "    page.place(pen.polygon(tri))\n",
    "    show(page)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "(You can also just pass in widget objects like `widgets.IntSlider()`, etc.)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Layouts and `interactive_output`\n",
    "\n",
    "A disadvantage of `widgets.interact` (either as a standalone function or as a decorator) is that it doesn't allow you to control the *layout* of the widgets. The ipywidgets library has a fairly sophisticated set of [classes and functions for styling widgets and arranging them in the notebook](https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20Styling.html). To use the layout functionality, however, you have to use a slightly more sophisticated method for hooking your function up to the widgets: the `widgets.interactive_output()` function. I show an example of this below."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Layouts with HBox and VBox\n",
    "\n",
    "The `widgets.HBox()` and `widgets.VBox()` functions produce horizontal and vertical layouts of widgets, respectively. Calling these functions with a list of widgets returns a layout object, which can be displayed like an individual widget. For example, using the widgets we defined back up at the top of the notebook:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 50,
   "metadata": {},
   "outputs": [],
   "source": [
    "hlayout = widgets.HBox([int_slider, float_slider, text_input, checkbox])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 51,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "9b2cca4fbbfd42d1a44ebaf566a60106",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(IntSlider(value=7, description='# of wugs', max=10), FloatSlider(value=0.5, description='dairy …"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "display(hlayout)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As you can see, the `HBox` positions the widgets so they're on a horizontal line. The `VBox` layout does the same, except vertically:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "e370330db8e340ec9d717f2c3a1441be",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "VBox(children=(IntSlider(value=7, description='# of wugs', max=10), FloatSlider(value=0.5, description='dairy …"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "vlayout = widgets.VBox([int_slider, float_slider, text_input, checkbox])\n",
    "display(vlayout)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "(This is the default behavior for `widgets.interact()`.) The tricky and useful thing about both `HBox` and `VBox` is that they can be nested: you can put an `HBox` inside of a `VBox` or a `VBox` inside of an `HBox`. The code in the cell below creates an `HBox` with two `VBox`es inside, creating a two-column layout of widgets:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 53,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "f6cb22051d074aef820e83b31b3872bb",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(VBox(children=(IntSlider(value=7, description='# of wugs', max=10), FloatSlider(value=0.5, desc…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "combo_layout = widgets.HBox([\n",
    "    widgets.VBox([int_slider, float_slider]),\n",
    "    widgets.VBox([text_input, checkbox]),\n",
    "])\n",
    "display(combo_layout)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Making it interactive with `interactive_output`\n",
    "\n",
    "Layouts are handy, but they're trickier to hook up to a function that you want to call whenever the widgets are updated. Let's start again with the `disp_tri()` function from above:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 351,
   "metadata": {},
   "outputs": [],
   "source": [
    "def disp_tri(stroke_width=10, height=20):\n",
    "    page = document(80, 80, 'mm').addpage()\n",
    "    pen = shape().nofill().stroke(rgb(0,0,0)).width(stroke_width)\n",
    "    tri = Polyline([(40, 20), (20, 20+height), (60, 20+height)])\n",
    "    page.place(pen.polygon(tri))\n",
    "    show(page)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As an example, I want to make an interface for this function where the widgets are side-by-side, instead of on top of each other. Doing this involves the folowing steps:\n",
    "\n",
    "* Create the widgets, assigning them to variables\n",
    "* Create a layout object (`HBox` and/or `VBox`)\n",
    "* Call the `widgets.interactive_output()` function. The first parameter is the function you want to make interactive; the second is a Python dictionary that maps that function's (named) parameters to the widget that should be used to control that parameter\n",
    "* Call `display()` with *both* the layout and the return value of `interactive_output` as parameters\n",
    "\n",
    "The code in the following cell implements this. I've left comments in order to point out the important parts:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 54,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "fd8f0ab038f541249b20eb5f0d0a698c",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(FloatSlider(value=0.5, continuous_update=False, description='stroke width', max=20.0, min=0.5),…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "12355ac1f7844085b2a6090397ad7326",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Output()"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# create widgets\n",
    "stroke_slider = widgets.FloatSlider(\n",
    "    description=\"stroke width\",\n",
    "    min=0.5, max=20, value=0.5, continuous_update=False)\n",
    "height_slider = widgets.FloatSlider(\n",
    "    description=\"height\",\n",
    "    min=20, max=60, continuous_update=False)\n",
    "\n",
    "# layout to set these side by side\n",
    "layout = widgets.HBox([stroke_slider, height_slider])\n",
    "\n",
    "# make interactive interface: first param is the function,\n",
    "# second is a dictionary mapping names of function\n",
    "# parameters to widgets\n",
    "output = widgets.interactive_output(\n",
    "    disp_tri,\n",
    "    {'stroke_width': stroke_slider, 'height': height_slider}\n",
    ")\n",
    "\n",
    "# display both the interface and the output\n",
    "display(layout, output)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Putting it all together\n",
    "\n",
    "Here's an example that puts it all together. The code in the cell below draws a number of ellipses at random positions, using parameters to determine the number of ellipses, the alpha value for the randomly-generated colors, the upper bound of the random number drawn for the ellipses' size, a random seed value, and a string that determines if the positions of the ellipses will be determined with a uniform distribution or with a normal distribution:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 55,
   "metadata": {},
   "outputs": [],
   "source": [
    "def rand_alpha_ellipses(n=10, alpha=255, max_size=40, pos_dist='uniform', seed=12345):\n",
    "    np.random.seed(seed)\n",
    "    page = document(80, 80, 'mm').addpage()\n",
    "    for i in range(n):\n",
    "        r = np.random.uniform(255)\n",
    "        g = np.random.uniform(255)\n",
    "        b = np.random.uniform(255)\n",
    "        if pos_dist == 'uniform':\n",
    "            x = np.random.uniform(80)\n",
    "            y = np.random.uniform(80)\n",
    "        else:            \n",
    "            x = np.random.normal(40, 20)\n",
    "            y = np.random.normal(40, 20)\n",
    "        size = np.random.uniform(max_size)\n",
    "        pen = shape().nostroke().fill(rgba(r, g, b, alpha))\n",
    "        page.place(pen.ellipse(x, y, size, size))\n",
    "    show(page)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 56,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/svg+xml": [
       "<svg height=\"226.7717pt\" version=\"1.1\" width=\"226.7717pt\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
       "<title>Untitled</title>\n",
       "<ellipse cx=\"180.963\" cy=\"99.637\" fill=\"rgba(18.8775,174.6406,208.2846,1)\" rx=\"47.5477\" ry=\"47.5477\"/>\n",
       "<ellipse cx=\"80.4132\" cy=\"59.3306\" fill=\"rgba(10.0133,89.093,64.7777,1)\" rx=\"7.1122\" ry=\"7.1122\"/>\n",
       "<ellipse cx=\"79.7769\" cy=\"45.4247\" fill=\"rgba(252.8694,227.9631,179.1293,1)\" rx=\"16.9657\" ry=\"16.9657\"/>\n",
       "<ellipse cx=\"66.1072\" cy=\"122.0589\" fill=\"rgba(9.9795,71.1839,91.8113,1)\" rx=\"77.3921\" ry=\"77.3921\"/>\n",
       "<ellipse cx=\"75.1946\" cy=\"49.6772\" fill=\"rgba(143.3303,69.659,2.5203,1)\" rx=\"94.4911\" ry=\"94.4911\"/>\n",
       "<ellipse cx=\"221.2457\" cy=\"116.6512\" fill=\"rgba(248.1803,51.706,25.4545,1)\" rx=\"55.2077\" ry=\"55.2077\"/>\n",
       "<ellipse cx=\"63.6859\" cy=\"43.5128\" fill=\"rgba(103.523,241.8028,27.6473,1)\" rx=\"58.0856\" ry=\"58.0856\"/>\n",
       "<ellipse cx=\"168.8349\" cy=\"121.9455\" fill=\"rgba(49.2119,230.624,199.3867,1)\" rx=\"62.6016\" ry=\"62.6016\"/>\n",
       "<ellipse cx=\"189.208\" cy=\"54.6058\" fill=\"rgba(74.7845,209.7745,120.0117,1)\" rx=\"10.7755\" ry=\"10.7755\"/>\n",
       "<ellipse cx=\"142.2702\" cy=\"36.7384\" fill=\"rgba(100.1886,216.8534,130.6348,1)\" rx=\"12.663\" ry=\"12.663\"/>\n",
       "</svg>"
      ],
      "text/plain": [
       "<IPython.core.display.SVG object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "rand_alpha_ellipses()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The code in the cell below creates sliders for each of these parameters, then set up a layout with these sliders. Finally, a call to `widgets.interactive_output` associates each of the widgets with the corresponding function parameter. This is a lot of work, but it produces a clean interface:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 57,
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "146e1b17280a4483bf84ca1c9bd045b1",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(VBox(children=(IntSlider(value=10, continuous_update=False, description='circle count', max=100…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "cce3be32ba5a4af79e4b748bfb8ebd5a",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Output()"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "number_slider = widgets.IntSlider(\n",
    "    description=\"circle count\", min=0, max=1000, value=10,\n",
    "    continuous_update=False)\n",
    "alpha_slider = widgets.IntSlider(\n",
    "    description=\"fill alpha\", min=0, max=255, value=127,\n",
    "    continuous_update=False)\n",
    "max_slider = widgets.IntSlider(\n",
    "    description=\"upper bound on size\", min=0, max=80, value=40,\n",
    "    continuous_update=False)\n",
    "pos_dist_select = widgets.Dropdown(\n",
    "    description=\"rand dist for position\",\n",
    "    options=[\"normal\", \"uniform\"],\n",
    "    value=\"normal\")\n",
    "seed_input = widgets.BoundedIntText(\n",
    "    description=\"random seed\",\n",
    "    min=0,\n",
    "    max=99999\n",
    ")\n",
    "layout = widgets.HBox([\n",
    "    widgets.VBox([number_slider, alpha_slider, max_slider]),\n",
    "    widgets.VBox([pos_dist_select, seed_input])\n",
    "])\n",
    "output = widgets.interactive_output(\n",
    "    rand_alpha_ellipses,\n",
    "    {\n",
    "        'n': number_slider,\n",
    "        'alpha': alpha_slider,\n",
    "        'max_size': max_slider,\n",
    "        'pos_dist': pos_dist_select,\n",
    "        'seed': seed_input\n",
    "    }\n",
    ")\n",
    "display(layout, output)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.8"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
